﻿namespace MetaProperties.Core
{
    using System;
    using System.ComponentModel;
    using System.Linq.Expressions;

    using MetaProperties.Framework;

    /// <summary>
    /// An observable value which is bound to a value on an object implementing INotifyPropertyChanged.
    /// </summary>
    /// <typeparam name="TSource">The type of the source.</typeparam>
    /// <typeparam name="TValue">The type of the value.</typeparam>
    public class BoundObservable<TSource, TValue> : GenericBoundObservable<TValue>
        where TSource : class, INotifyPropertyChanged
    {
        /// <summary>
        /// The name of the bound property.
        /// </summary>
        private string propertyName;

        /// <summary>
        /// The source containing the bound property.
        /// </summary>
        private TSource source;

        /// <summary>
        /// The method which gets the value of the bound property.
        /// </summary>
        private Func<TSource, TValue> getValueMethod;

        /// <summary>
        /// The method which sets the value of the bound property.
        /// </summary>
        private Action<TValue> setValueMethod;

        /// <summary>
        /// Initializes a new instance of the <see cref="BoundObservable&lt;TSource, TValue&gt;"/> class.
        /// </summary>
        public BoundObservable()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BoundObservable&lt;TSource, TValue&gt;"/> class.
        /// </summary>
        /// <param name="defaultValue">The default value.</param>
        public BoundObservable(TValue defaultValue)
        {
            this.DefaultValue = defaultValue;
        }

        /// <summary>
        /// Gets a value indicating whether the item is currently bound.
        /// </summary>
        /// <value><c>true</c> if the source is set; otherwise, <c>false</c>.</value>
        protected override bool IsBound
        {
            get { return this.source != null; }
        }

        /// <summary>
        /// Binds to the specified source.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="propertyExpression">The property expression.</param>
        public void Bind(TSource source, Expression<Func<TSource, TValue>> propertyExpression)
        {
            this.Bind(source, propertyExpression, null);
        }

        /// <summary>
        /// Binds to the specified source.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="propertyExpression">The property expression.</param>
        /// <param name="setValueMethod">The set value method.</param>
        public void Bind(TSource source, Expression<Func<TSource, TValue>> propertyExpression, Action<TValue> setValueMethod)
        {
            string name = PropertyNameHelper.GetProperty(propertyExpression).Name;
            Func<TSource, TValue> method = propertyExpression.Compile();
            this.Bind(source, name, method, setValueMethod);
        }

        /// <summary>
        /// Binds to the specified source.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="getValueMethod">The get value method.</param>
        public void Bind(TSource source, string propertyName, Func<TSource, TValue> getValueMethod)
        {
            this.Bind(source, propertyName, getValueMethod, null);
        }

        /// <summary>
        /// Binds to the specified source.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="getValueMethod">The get value method.</param>
        /// <param name="setValueMethod">The set value method.</param>
        public void Bind(TSource source, string propertyName, Func<TSource, TValue> getValueMethod, Action<TValue> setValueMethod)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            this.Unbind();

            this.source = source;
            this.propertyName = propertyName;
            this.getValueMethod = getValueMethod;
            this.setValueMethod = setValueMethod;

            this.ObservableValue.Value = this.getValueMethod(this.source);

            this.source.PropertyChanged += this.Source_PropertyChanged;

            if (this.setValueMethod != null)
            {
                this.ObservableValue.ValueChanged += this.ObservableValue_ValueChanged;
            }
        }

        /// <summary>
        /// Unbinds from the source.
        /// </summary>
        public override void Unbind()
        {
            if (this.source != null)
            {
                this.source.PropertyChanged -= this.Source_PropertyChanged;

                if (this.setValueMethod != null)
                {
                    this.ObservableValue.ValueChanged -= this.ObservableValue_ValueChanged;
                }

                this.source = null;
                this.propertyName = null;
                this.getValueMethod = null;
                this.setValueMethod = null;

                this.ObservableValue.Value = this.DefaultValue;
            }
        }

        /// <summary>
        /// Handles the PropertyChanged event of the source.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void Source_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == this.propertyName)
            {
                this.ObservableValue.Value = this.getValueMethod(this.source);
            }
        }

        /// <summary>
        /// Handles the ValueChanged event of the ObservableValue object.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="MetaProperties.Framework.ValueChangedEventArgs&lt;TValue&gt;"/> instance containing the event data.</param>
        private void ObservableValue_ValueChanged(object sender, ValueChangedEventArgs<TValue> e)
        {
            if (this.setValueMethod != null)
            {
                this.setValueMethod(e.NewValue);
            }
        }
    }
}
